package darknet.application.chat.impl;

import darknet.application.chat.impl.net.ChatMessageFactory;
import darknet.application.chat.impl.net.response.JoinChannelResponse;
import darknet.application.chat.spec.Channel;
import darknet.application.chat.spec.ChannelObserver;
import darknet.application.chat.impl.net.message.BroadcastChannelMessage;
import darknet.application.chat.impl.net.message.BroadcastExitChannelMessage;
import darknet.application.chat.impl.net.message.BroadcastJoinChannelMessage;
import darknet.application.chat.impl.net.message.CloseChannelMessage;
import darknet.application.chat.impl.net.message.JoinChannelMessage;
import darknet.core.spec.net.NetworkManager;
import darknet.core.spec.net.Node;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Distant implementation of hosted channel.
 *
 * This channel can receive (for now) 4 messages :
 *
 * - Broadcast join channel message :
 *  Sent by the channel owner when a node joined the channel.
 *
 * - Broadcast channel message :
 *  This message is sent by the channel owner to notify connected nodes a
 *  new message has been sent.
 *
 * - Broadcast exit channel message :
 *   Sent by the channel owner when a node exit the channel.
 *
 * - Close channel message :
 *  This message is sent by the channel owner to notify connected nodes the
 *  channel is now closed.
 * 
 * @author yoyo
 */
public class ChannelDistantImpl implements Channel {

    /** NetworkManager */
    transient private NetworkManager network = null;

    /** Chat message factory */
    transient private ChatMessageFactory factory = null;

    /** Chat Application impl */
    transient private ChatApplicationImpl application = null;

    /** Channel hoster */
    private Node hoster = null;

    /** Channel name */
    private String channelName = null;

    /** Nodes connected to this channel. */
    private Set<Node> connectedNodes = null;

    /** Observers */
    transient private Set<ChannelObserver> observers;

    /**
     * Initialize a distant version of a channel.
     * 
     * @param network
     * @param hoster
     * @param channelName
     */
    public ChannelDistantImpl(NetworkManager network, Node hoster,
            String channelName, ChatApplicationImpl application) {

        if (network == null) {
            throw new IllegalArgumentException("network is null");
        }
        this.network = network;

        if (hoster == null) {
            throw new IllegalArgumentException("hoster is null");
        }
        this.hoster = hoster;

        if (channelName == null || channelName.length() == 0) {
            throw new IllegalArgumentException("channelName is invalid");
        }
        this.channelName = channelName;

        if (application == null) {
            throw new IllegalArgumentException("application is invalid (null)");
        }
        this.application = application;

        factory = ChatMessageFactory.getInstance(network);
        connectedNodes = new HashSet<Node>(10);
        observers = new HashSet<ChannelObserver>(1);
    }

    @Override
    public void join() throws IOException {
        
        JoinChannelMessage message = factory.makeJoinChannelMessage(
                hoster, channelName);
        JoinChannelResponse response = (JoinChannelResponse) network.send(message);
        application.addJoinedChannel(this);

        connectedNodes.addAll(response.getConnectedNodes());
    }

    @Override
    public void speak(String message) throws IOException {
        network.send(factory.makeChannelMessage(message, hoster, channelName));
    }

    @Override
    public void exit() throws IOException {
        network.send(factory.makeExitChannelMessage(hoster, channelName));
    }

    @Override
    public void close() {
        throw new UnsupportedOperationException(
                "A distant node cannot be closed");
    }

   @Override
    public Set<Node> getNodes() {
       HashSet<Node> nodes =  new HashSet<Node>(connectedNodes);
       nodes.add(hoster);
       nodes.add(network.myNode());
       return nodes;
    }

    @Override
    public Node getHoster() {
        return hoster;
    }

    @Override
    public String getName() {
        return channelName;
    }

    @Override
    public void addChannelObserver(ChannelObserver toAdd) {
        if (toAdd == null) {
            throw new IllegalArgumentException("toAdd is null");
        }
        observers.add(toAdd);
    }

    @Override
    public void removeChannelObserver(ChannelObserver toRemove) {
        if (toRemove == null) {
            throw new IllegalArgumentException("toRemove is null");
        }
        observers.remove(toRemove);
    }

    @Override
    public List<ChannelObserver> getChannelObservers() {
        return new ArrayList<ChannelObserver>(observers);
    }

    /**
     * Handle broadcast join channel message.
     * 
     * @param message
     */
    void newBroadcastJoinMessage(BroadcastJoinChannelMessage message) {

        // Send the event to all observers.
        for (ChannelObserver observer : getChannelObservers()) {
            observer.newBroadcastJoinChannelMessage(message);
        }
    }

    /**
     * Handle broadcast channel message.
     *
     * @param message
     */
    void newBroadcastChannelMessage(BroadcastChannelMessage message) {

        // Send the event to all observers.
        for (ChannelObserver observer : getChannelObservers()) {
            observer.newBroadcastChannelMessage(message);
        }
    }

    /**
     * Handle broadcast join channel message.
     *
     * @param message
     */
    void newBroadcastExitMessage(BroadcastExitChannelMessage message) {

        // Send the event to all observers.
        for (ChannelObserver observer : getChannelObservers()) {
            observer.newBroadcastExitChannelMessage(message);
        }
    }

    /**
     * Handle close channel message.
     *
     * @param message
     */
    void newCloseChannelMessage(CloseChannelMessage message) {

        // Send the event to all observers.
        for (ChannelObserver observer : getChannelObservers()) {
            observer.newCloseChannelMessage(message);
        }
    }
}
